<?php
/**
 * Класс обеспечивает соединение с базой данных
 * Автоматически формирует массивы
 * Обеспечивает обработку исключительных ситуаций при осуществлении запросов
 *
 * @author Дмитрий Кузьма <kouzma.d@gmail.com>
 * @version 1.0
 */

class Service_Database {
  private $hConn;   //Дескриптор соединения с базой данных
  public static $objDB;
  private static $_stmtCounter = 0;
  private static $_arrStmtCounter = array();
  
  /**
   * Конструктор - устанавливаем соединение с БД.
   *
   */
  private function __construct() {
    $hostName  = DB_HOST . ":" . DB_PORT;

    $this->hConn = @mysql_pconnect($hostName, DB_USER, DB_PASSWORD);
    if (! is_resource($this->hConn)) {
      throw new Exception_Database('Unable to connect to the database ' .
      	'Host: "' . $hostName . '"; User: "' . DB_USER. '"', E_USER_ERROR);
    }
    $curBD = @mysql_select_db(DB_NAME);//Выбираем базу данных для работы
    if (! ($curBD)) {
      throw new Exception_Database('Unable to select database Database: "' . DB_NAME . '"', E_USER_ERROR);
    }
    @mysql_query("SET NAMES 'utf8'");
  }

  /**
   * Функция обеспечивает работу шаблона Singleton.
   * Благодаря ей у нас всегда будет только одно подключение к базе
   *
   * @return Service_Database
   */
  static public function instance() {
    if (!isset(self::$objDB)) self::$objDB = new Service_Database();
    return self::$objDB;
  }

  /**
   * Получает информацию о последнем вставленном идентификаторе
   *
   * @return integer - значение последнего вставленно идентификатора (false, если не было удачного запроса; 0 если в таблице нет autoincrement)
   */
  public function getLastInsertId() {  	
    $stmt = "SELECT LAST_INSERT_ID() AS LID";
    $this->_debug();
    $hRes = mysql_query($stmt, $this->hConn);
    if (!$hRes) $this->_checkErrors($stmt); //Проверяем, корректно ливыполнен запрос
    $arrLID = mysql_fetch_assoc($hRes);
    $this->_debugTime($stmt);
    return $arrLID['LID'];
  }
  
  public function getAffectedRows() {
  	$this->_debug();
  	$hRes = mysql_affected_rows($this->hConn);
  	$this->_debugTime('#AFFECTED ROWS');
  	return $hRes;
  }
  
  /**
   * Вставляет в определенную таблицу массив значений
   *
   * @param string $table - название таблицы
   * @param array $arFieldValues - массив вставляемых значений (ключ соответствует полю в БД)
   * @param bool $ignore - вставлять ли IGNORE
   * @return integer - количество вставленных строк
   */
  public function insert($table, $arFieldValues, $ignore = false) {  	
    $fields = array_keys($arFieldValues);     //Массив названий элементов таблицы
    $values = array_values($arFieldValues);   //Массив значений
		for ($i = 0; $i < sizeof($fields); $i++) {
		  $fields[$i] = '`' . $fields[$i] . '`';
		}

    $escVals = array();   //Выходной массив значений
    foreach($values as $val) {  //Проходим массив значений поэлементно
      //if (! is_numeric($val)) {  //Если текущее значение - не число
        if ($val === null) $val = 'null';
        else $val = "'" . $val . "'";//Приводим к корректному выводу
      //}
      $escVals[] = $val;  //Записываем корректное значение в массив
    }
    //Генерируем SQL запрос
    $stmt = " INSERT " . ($ignore === true ? "IGNORE " : "") . "INTO `". $table . "` ( ";
    	$stmt .= join(", ", $fields);
    $stmt .= ' ) VALUES( ';
    	$stmt .= join(', ', $escVals);
    $stmt .= ' )';
    if ($ignore === 'update') { 
    	$stmt .= " ON DUPLICATE KEY UPDATE ";
    	$i = 0;
    	foreach ($fields as $key => $field) {
    		if ($i++)$stmt .= ', ';
    		$stmt .= $field . " = VALUES(" . $field . ")";
    	}
    }
    $this->_debug();
    $hRes = @mysql_query($stmt, $this->hConn);
    if (!$hRes) $this->_checkErrors($stmt); //Проверяем, корректно ли выполнен запрос
    $result = mysql_affected_rows($this->hConn);
    $this->_debugTime($stmt, $result);
    return $result;
  }

  /**
   * Вставляет в определенную таблицу массив значений
   *
   * @param string $table - название таблицы
   * @param array $arFieldValues - массив вставляемых значений (ключ соответствует полю в БД)
   * @param bool $ignore - вставлять ли IGNORE
   * @return integer - идентификатор вставленого элемента
   */
  public function insertAutoincrement($table, $arFieldValues, $ignore = false) {  	
    if ($this->insert($table, $arFieldValues, $ignore)) return $this->getLastInsertId();
    return false;
  }
/**
   * Выполняет запрос на вставку значений
   *
   * @param string $stmt - строка запроса
   * @return integer - идентификатор
   */
  public function queryInsertAutoincrement($stmt) {  	
    if ( $this->query($stmt) > 0) return $this->getLastInsertId();
    return false;
  }

  /**
   * Удаляет из таблицы все элементы, удовлетворяющие условию
   *
   * @param string $table - название таблицы
   * @param array $arConditions - массив условий (ключ соответствует полю в БД)
   * @return integer - количество удаленных строк
   */
  function delete($table, $arConditions) {  	
    //Создаем строку WHERE
    $arWhere = array();//Массив элементов для WHERE
    foreach($arConditions as $field => $val) {  //Проходим массив значений поэлементно
      if ($val === null) {
      	$arWhere[] = $field . " IS NULL" ;//Записываем корректное значение в массив
      }
      else {
		  	//if (! is_numeric($val)) {  //Если текущее значение - не число
		      //$val = "'" . absolutelyEscapeString($val) . "'";//Приводим к корректному выводу
		      $val = "'" . $val . "'";//Приводим к корректному выводу
		    //}
		    $arWhere[] = "`" . $field . "` = " . $val;//Записываем корректное значение в массив
      }
    }
    //Формируем запрос на удаление
    $stmt = "DELETE FROM `" . $table . "` WHERE " . join(' AND ', $arWhere);
    $this->_debug();
    $hRes = @mysql_query($stmt, $this->hConn);
    if (!$hRes) $this->_checkErrors($stmt); //Проверяем, корректно ливыполнен запрос
    $result = mysql_affected_rows($this->hConn);
    $this->_debugTime($stmt, $result);
    return $result;    
  }

  /**
   * выполняет любой запрос SQL типа SELECT
   *
   * @param string $stmt - строка запроса
   * @return array - двумерный ассоциативный массив
   */
  public function select($stmt) {
  	$this->_debug();
    $hRes = @mysql_query($stmt, $this->hConn);    
    if (!$hRes) $this->_checkErrors($stmt); //Проверяем, корректно ливыполнен запрос
    $arReturn = array();//Массив для возвращения функцией (результат)
    while($row = mysql_fetch_assoc($hRes)) {
      $arReturn[] = $row;
    }
    $this->_debugTime($stmt, $arReturn);
    return $arReturn;
  }

  /**
   * выполняет любой запрос SQL типа SELECT для получения информации об одном поле и возвращает простой одномерный массив
   *
   * @param string $stmt - строка запроса
   * @return array - одномерный индексный массив
   */
  public function selectSimpleArray($stmt) {
  	$this->_debug();
    $hRes = @mysql_query($stmt, $this->hConn);    
    if (!$hRes) $this->_checkErrors($stmt); //Проверяем, корректно ливыполнен запрос
    if ( mysql_num_rows($hRes) == 0 ) return array();
    $arReturn = array();//Массив для возвращения функцией (результат)
    while($row = mysql_fetch_array($hRes)) {
      $arReturn[] = $row[0];
    }
    $this->_debugTime($stmt, $arReturn);
    return $arReturn;
  }
  /**
   * выполняет любой запрос SQL типа SELECT для получения информации об одном поле одной строки
   *
   * @param string $stmt - строка запроса
   * @return mixed - значение единственного поля
   */

  public function selectSimpleField($stmt) {
  	$this->_debug();
    $hRes = @mysql_query($stmt, $this->hConn);    
    if (!$hRes) $this->_checkErrors($stmt); //Проверяем, корректно ливыполнен запрос
    if ( mysql_num_rows($hRes) == 0 ) return null;
    $arTemp =  mysql_fetch_array($hRes);
    $this->_debugTime($stmt, $arTemp);
    return $arTemp[0];
  }

  /**
   * выполняет любой запрос SQL типа SELECT, возвращает только первый элемент
   *
   * @param string $stmt - строка запроса
   * @return array - двумерный ассоциативный массив
   */
  public function selectOne($stmt) {
  	$this->_debug();
    $hRes = @mysql_query($stmt, $this->hConn);    
    if (!$hRes) $this->_checkErrors($stmt); //Проверяем, корректно ливыполнен запрос
    if ( mysql_num_rows($hRes) == 0 ) return array();
    $arrResult =  mysql_fetch_assoc($hRes);
    $this->_debugTime($stmt, $arrResult);
    return $arrResult;
  }

  /**
   * Выполняет произвольный SQL запрос
   *
   * @param string $stmt - строка запроса
   * @return integer - количество обработанных строк
   */
  public function query($stmt) {  	
  	$this->_debug();
    $hRes = @mysql_query($stmt, $this->hConn);
    if (!$hRes) $this->_checkErrors($stmt); //Проверяем, корректно ливыполнен запрос
    $result = @mysql_affected_rows($this->hConn);
    $this->_debugTime($stmt, $result);
    return $result;
  }

  //Функция обновляет в таблице $table поля $arFieldValues, но только те элементы, которые соотвествуют условию $arConditions
  /**
   * Обновляет в таблице определенные поля, согласно условию
   *
   * @param string $table - название таблицы
   * @param array $arFieldValues - массив обновляемых значений (ключ соответствует полю в БД)
   * @param array $arConditions - массив условий (ключ соответствует полю в БД)
   * @return integer - количество обновленных строк
   */
  public function update($table, $arFieldValues, $arConditions) {
    // Формируем корректный массив значений
    $arUpdates = array();//Массив элементов для значений
    foreach($arFieldValues as $field => $val) {//Проходим массив значений поэлементно
    	//if (! is_numeric($val)) { //Если текущее значение - не число
        if ($val === null) $val = 'null';
        else $val = "'" . $val . "'";//Приводим к корректному выводу
      //}
      $arUpdates[] = "`" . $field . "` = " . $val;//Записываем корректное значение в массив
    }

    // Формируем корректный массив WHERE
    $arWhere = array();//Массив элементов для WHERE
    foreach($arConditions as $field => $val) {//Проходим массив WHERE поэлементно
      if ($val === null) {
      	$arWhere[] = "`" . $field . "` IS NULL" ;//Записываем корректное значение в массив
      }
      else {
      	if (! is_numeric($val)) { //Если текущее значение - не число
	        $val = "'" . $val . "'";//Приводим к корректному выводу
      	}
      	$arWhere[] = "`" . $field . "` = " . $val;//Записываем корректное значение в массив
      }
    }
    //Формируем SQL-запрос на обновление данных
    $stmt  = "UPDATE `" . $table . "` SET ";
    $stmt .= join(', ', $arUpdates);
    
    $stmt .= ' WHERE ' . join(' AND ', $arWhere);
    //$stmt .= ' LIMIT 1';
    $this->_debug();
    $hRes = mysql_query($stmt, $this->hConn);//Выполняем запрос
    if (!$hRes) $this->_checkErrors($stmt); //Проверяем, корректно ливыполнен запрос
    $result = mysql_affected_rows($this->hConn);
    $this->_debugTime($stmt, $result);
    return $result;
  }

  /**
   * Деструктор - закрываем соединение с базой данных
   *
   */
  public function __destruct() {
    if (is_resource($this->hConn)) {
      @mysql_close($this->hConn);
    }
  }
  
  /**
   * Проеряет, существует ли запись в БД
   *
   * @param string $stmt
   * @return bool
   */
  public function isRecordExist($stmt) {
  	$this->_debug();
    $hRes = @mysql_query($stmt, $this->hConn);    
    if (!$hRes) $this->_checkErrors($stmt); //Проверяем, корректно ливыполнен запрос
    $result = mysql_num_rows($hRes);
    $this->_debugTime($stmt, $result);
    if ($result > 0) return true;
    else return false;
  }

  /**
   * Начинает транзакцию
   *
   */
  public function startTransaction() {
  	$this->query("SET AUTOCOMMIT=0;");
  	$this->query("START TRANSACTION;");
  }
  
  /**
   * Завершает транзакцию
   *
   */
  public function stopTransaction() {
  	$this->query("COMMIT;");
  	$this->query("SET AUTOCOMMIT=1;");
  }

  /**
   * Отменяет транзакцию
   *
   */
  public function cancelTransaction() {
  	$this->query("ROLLBACK;");
  }

  /**
   * Ищет таблицы в которых есть нужные значения
   * 
   * @param string $search - что ищем
   * @param bool $break - Остановить поиск при первом вхождении
   * @return boolean
   */
  public function searchTable($search, $break = false) {  	
  	$stmt = "SHOW TABLES;";
	  $arrTables = $this->select($stmt);
	  foreach ($arrTables as $table) {
			$table = $table['Tables_in_hotels'];
		  $stmt = "DESCRIBE " . $table;
		  $arrFields = $this->select($stmt);
		  foreach ($arrFields as $arrField) {
		  	$stmt = "SELECT COUNT(*) FROM  " . $table . " WHERE " . $arrField['Field'] . " LIKE '" . $search . "'";
		  	if ($this->selectSimpleField($stmt)) {
		  		echo '<b>Table:</b> ' . $table . ' <b>Field:</b> ' . $arrField['Field'] . "<br />";
		  		if ($break) return !1;
		  	}
		  }
	  }
  }

  /**
   * Ищем таблицы где есть определенное поле
   * 
   * @param string $field - Название поля
   * @return boolean
   */
  public function searchField($field) {  	
  	$stmt = "SHOW TABLES;";
	  $arrTables = $this->select($stmt);
	  $i = 0;
	  foreach ($arrTables as $table) {
			$table = $table['Tables_in_' . DB_NAME];
		  $stmt = "DESCRIBE " . $table;
		  $arrFields = $this->select($stmt);
		  $arrFields = Service_ArrayConverter::getColumn($arrFields, 'Field');
		  foreach ($arrFields as $tableField) {
		  	if (preg_match('#' . $field . '#', $tableField)) {
		  		echo ++$i . '. <b>Table:</b> ' . $table . ' <b>Field:</b> ' . $tableField . "<br />";
		  	}
		  }
		  
	  }
  }
  
  /**
   * Формирует INSERT запрос для вставки нескольких значений 
   * ()
   * 
   * @param string $table - Название таблицы для вставки
   * @param mixed $fieldName - Название поля, которое принимает разные значения (Может быть массив)
   * @param array $arrEssenceIds - Массив значений поля
   * @param array $arrOtherFields - Ассоциативный массив с остальными полями и значениями (Sample: array('ID_hotel' => 15, 'ID_room' => 12))
   * @param bool $ignore - Использовать ли слово IGNORE 
   */
  public static function makeMultipleInsertString($table, $fieldName, array $arrEssenceIds, array $arrOtherFields, $ignore = false) {  	
  	if (empty($arrEssenceIds)) return false;
  	$i = 0;
		$str = '';
		// Оставляем только нужные поля если несколько полей
		if (is_array($fieldName))	$arrEssenceIds = Service_ArrayConverter::getColumnsOfMultyArray($arrEssenceIds, $fieldName);
		foreach ($arrEssenceIds as $essenceId) {
			if ($i++) $str .= ',';
			$dinamicFieldsValues = is_array($fieldName) ? join("','", $essenceId) : $essenceId;
			$str .= "('" . $dinamicFieldsValues . "'" . (!empty($arrOtherFields) ? ", '" . join("','", array_values($arrOtherFields)) . "'" : '') . ")";
		}  
		$dinamicFields = is_array($fieldName) ? join('`,`', $fieldName) : $fieldName;
		$stmt = "INSERT " . ($ignore === true ? " IGNORE " : "") . " INTO `" . $table . "`
			(`" . $dinamicFields . "`" . (!empty($arrOtherFields) ? ", `" . join("`,`", array_keys($arrOtherFields)) . "`": '') . ") 
			VALUES " . $str;
		if ($ignore === 'update') {
			$stmt .= " ON DUPLICATE KEY UPDATE ";
			$i = 0;
			$arrFields = $fieldName;
			if (!is_array($arrFields)) $arrFields = array($arrFields);
			foreach ($arrFields as $key => $field) {
				if ($i++)$stmt .= ', ';
				$stmt .= $field . " = VALUES(" . $field . ")";
			}
		}
		return $stmt;	
  }
  
  /**
   * Обрабатываем ошибки в БД
   * 
   * @param string $stmt - Запрос
   * @throws Exception_Database
   */
  private function _checkErrors($stmt) {
  	$err = mysql_error($this->hConn);
  	// Сохраняем ошибку
  	echo $stmt;
  	echo '<br><br>';
  	Error::saveError('Database error', $err . ' - ' . $stmt);
  	// Если Deadlock повторяем запрос 	
  	if (preg_match('#Deadlock found when trying to get lock; try restarting transaction#', $err)) {
  		$this->query($stmt);
  	}
  	elseif (preg_match('#Lock wait timeout exceeded; try restarting transaction#', $err)) {
  		$this->query($stmt);
  	}
  	else {
  		$arrDebug = debug_backtrace();
  		$errDebug = '<br>';
  		for ($i = 1 ; $i <= 3 ; $i++) {
	  		$errDebug .= "\n Class: " . (@$arrDebug[$i]['class']) . ", Function: " . @$arrDebug[$i]['function'] . " <br>\n";
	  		$errDebug .= 'File: ' . (@$arrDebug[$i]['file']) . ", " . @$arrDebug[$i]['line'] . " <br>\n";
  		}
  		throw new Exception_Database($err . ' - ' . $stmt . "\n" . $errDebug);
  	}
  }
  
  /**
   * Выводим запросы для отладки
   * 
   */
  private function _debug() {
  	Pressure::setStart('db' . $this::$_stmtCounter);	
  }
  
  /**
   * Подсчитываем и выводим данные по времени
   * 
   * @param string $stmt - Запрос
   * @param array $arrResult - Результат
   */
  private function _debugTime($stmt, $arrResult = null) {
  	if (defined('DEBUG_OFF')) return;
  	$stmt = trim($stmt);
  	$pos =  strpos($stmt, ' ');
  	$type = substr($stmt, 0, $pos);
  	$this::$_stmtCounter++;
  	$this::$_arrStmtCounter[$type] = isset($this::$_arrStmtCounter[$type]) ? $this::$_arrStmtCounter[$type] + 1 : 1;
  	$time = Pressure::calculateCurrentDuration('db' . ($this::$_stmtCounter - 1));
  	
  	// Логируем для отладки
  	$arrTrace = debug_backtrace(2);
  	$arrCalled[0] = $arrTrace[1]['class'] . $arrTrace[1]['type'] . $arrTrace[1]['function']; 
  	$arrCalled[0] .= ' ' . $arrTrace[1]['file'] . ' (' . $arrTrace[1]['line'] . ')'; 
  	if (isset($arrTrace[2])) {
  		$arrCalled[1] = (isset($arrTrace[2]['class']) ? $arrTrace[2]['class'] : 'Class') . (isset($arrTrace[2]['type']) ? $arrTrace[2]['type'] : 'Type') . (isset($arrTrace[2]['function']) ? $arrTrace[2]['function'] : 'Function');
  		$arrCalled[1] .= ' ' . (isset($arrTrace[2]['file']) ? $arrTrace[2]['file'] : 'File') . ' (' . (isset($arrTrace[2]['line']) ? $arrTrace[2]['line'] : 'Line') . ')';
  	}  	 
  	if (isset($arrTrace[3])) {
  		$arrCalled[2] = (isset($arrTrace[3]['class']) ? $arrTrace[3]['class'] : 'Class') . (isset($arrTrace[3]['type']) ? $arrTrace[3]['type'] : 'Type') . (isset($arrTrace[3]['function']) ? $arrTrace[3]['function'] : 'Function');
  		$arrCalled[2] .= ' ' . (isset($arrTrace[3]['file']) ? $arrTrace[3]['file'] : 'File') . ' (' . (isset($arrTrace[3]['line']) ? $arrTrace[3]['line'] : 'Line') . ')';
  	}  	 
  	if (isset($arrTrace[4])) {
  		$arrCalled[3] = (isset($arrTrace[4]['class']) ? $arrTrace[4]['class'] : 'Class') . (isset($arrTrace[4]['type']) ? $arrTrace[4]['type'] : 'Type') . (isset($arrTrace[4]['function']) ? $arrTrace[4]['function'] : 'Function');
  		$arrCalled[3] .= ' ' . (isset($arrTrace[4]['file']) ? $arrTrace[4]['file'] : 'File') . ' (' . (isset($arrTrace[4]['line']) ? $arrTrace[4]['line'] : 'Line') . ')';
  	}  	 
  	$GLOBALS['db_requests'][$this::$_stmtCounter - 1]['stmt'] = $stmt;
  	$GLOBALS['db_requests'][$this::$_stmtCounter - 1]['time'] = $time;
  	$GLOBALS['db_requests'][$this::$_stmtCounter - 1]['type'] = $type;
  	$GLOBALS['db_requests'][$this::$_stmtCounter - 1]['called'] = $arrCalled;
  	if ($type == 'SELECT') {
	  	if ($arrResult && strlen(print_r($arrResult, true)) < 1024 * 25) {	  		
	  		$GLOBALS['db_requests'][$this::$_stmtCounter - 1]['result'] = $arrResult;
	  	}
  	}
  	else $GLOBALS['db_requests'][$this::$_stmtCounter - 1]['rows'] = $arrResult;
  	
/*  	// Для вывода
  	if (isset($_GET['stmt']) && SimpleAdministration::isAdmin()) {
  		if (!isset($_GET['slow']) || $time > 0.05) {
  			$arrDebug = debug_backtrace();
  			echo @$arrDebug[2]['class'] . @$arrDebug[2]['type'] . @$arrDebug[2]['function'] . " <br>\n";
  			echo @$arrDebug[2]['file'] . ' (' . @$arrDebug[2]['line'] . ") <br>\n";
  			if (isset($_GET['full'])) {
  				echo @$arrDebug[3]['class'] . @$arrDebug[3]['type'] . @$arrDebug[3]['function'] . " <br>\n";
  				echo @$arrDebug[3]['file'] . ' (' . @$arrDebug[3]['line'] . ") <br>\n";
  			}
  			echo $stmt . " <br>\n";
  			echo 'Time: ' . $time . "<br>\n";
  			echo "<br>\n";
  			if (isset($_GET['stmtresult']) && $arrResult) {
  				echo "Result: <br>\n";
  				echo '<pre>';
  				print_r($arrResult);
  				echo '</pre>';
  			}
  		}
  	}*/
  }
  
  /**
   * Возвращаем кол-во запросов
   */
  public static function getStmtCount() {
  	echo '<pre>';
  	print_r(self::$_arrStmtCounter);
  	echo '</pre>';
    return self::$_stmtCounter;
  }
}
?>